 /*
  * Professional Android, 4th Edition
  * Reto Meier and Ian Lake
  * Copyright 2018 John Wiley Wiley & Sons, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */

package com.professionalandroid.apps.myapplication;

 /*
  * 清单11-8：使用异步任务的作业服务 Listing 11-8: A Job Service using an Async Task
  */
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import com.firebase.jobdispatcher.Constraint;
import com.firebase.jobdispatcher.FirebaseJobDispatcher;
import com.firebase.jobdispatcher.GooglePlayDriver;
import com.firebase.jobdispatcher.Lifetime;
import com.firebase.jobdispatcher.Trigger;
import com.professionalandroid.apps.earthquake.Earthquake;
import com.professionalandroid.apps.earthquake.EarthquakeDatabaseAccessor;
import com.professionalandroid.apps.earthquake.EarthquakeMainActivity;
import com.professionalandroid.apps.earthquake.EarthquakeUpdateJobService;
import com.professionalandroid.apps.earthquake.PreferencesActivity;
import com.professionalandroid.apps.earthquake.R;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)//这里的标识注明只有在Android 5.0及以上才可以
public class BackgroundJobService extends JobService {
  private AsyncTask<Void, Void, Boolean> mJobTask;

   private static final String AsyncTaskTAG = "EqJobServiceAsyncTask";
   /**
    * 定期作业
    */
   private static final int PERIODIC_UPDATE_TAG_JOB_ID = 132;
  /*
   * 清单11-10：调度需要未计量网络和充电的作业 Listing 11-10: Scheduling a job that requires unmetered network and charging
   */
   /**
    * 一次性作业
    * 可以是任何整数，只需在整个应用程序中保持唯一即可 Can be any integer, just needs to be unique across your app
    */
    private static final int BACKGROUND_UPDATE_JOB_ID = 131;
   /**
    * 执行调度计划任务,后台更新地震数据，从网络上获取，添加进数据库
    * @param context 应用程序上下文
    */
  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  public static void scheduleEarthquakeUpdateJob(Context context) {

    // 访问Job Scheduler。 Access the Job Scheduler
    JobScheduler jobSchedulerwxz = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);

    // 获取 my Job Service 实现的引用. Get a reference to my Job Service implementation
    ComponentName jobServiceName = new ComponentName(
            context, BackgroundJobService.class);

    // 构建Job Info以运行这个Job Service。 Build a Job Info to run my Job Service
    // 清单11-11：使用自定义的退避条件安排作业 Listing 11-11: Scheduling a job with customized back-off criteria
    jobSchedulerwxz.schedule(
            new JobInfo.Builder(BACKGROUND_UPDATE_JOB_ID, jobServiceName)
                    //需要网络连接 Require a network connection
                    //.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)//需要不计量的网络连接，通常是wifi
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意的网络连接
//                    //要求设备处于空闲状态  Require the device has been idle
//                    .setRequiresDeviceIdle(true)
//                    .setRequiresCharging(true)//限制作业仅在设备充电时运行
                    // 强制作业在1天后忽略约束 TimeUnit.DAYS.toMillis(1)
                    .setOverrideDeadline(0L) // mine：王羲之 设置启动后多长时间范围内随机开始执行任务
//                    // 30秒后重试，线性后退   Retry after 30 seconds, with linear back-off
//                    .setBackoffCriteria(30000, JobInfo.BACKOFF_POLICY_LINEAR)
                   //设置是否在设备重新启动期间保留此作业，@see android.app.job.JobInfo 需要权限  Reschedule after the device has been rebooted
               //   .setPersisted(true)
//       设置延迟执行时间
//        builder.setMinimumLatency(TimeUnit.MILLISECONDS.toMillis(10));
//      不重复      .setPeriodic(TimeUnit.MINUTES.toMillis(1)) //重复执行设置 updateFreq*60*1000 TimeUnit.MINUTES.toMillis(updateFreq)

                    .build());
  }







  @SuppressLint("StaticFieldLeak")// TODO StaticFieldLeak
  @Override
  public boolean onStartJob(final JobParameters params) {
    // TODO直接在主线程上执行工作   Do work directly on the main Thread
    //返回false；如果没有耗时的工作要做。 // return false; // if no time consuming work remains to be done.
    Toast.makeText(this,"正在执行JobService任务ID"+params.getJobId() ,Toast.LENGTH_LONG).show();
    // 在后台线程中执行其他工作。 // Execute additional work within a background thread.
    mJobTask = new AsyncTask<Void, Void, Boolean>() {
      @Override
      protected Boolean doInBackground(Void... voids) {
        //   Do your background work.





//加载地震数据！
        // Result ArrayList of parsed earthquakes.
        ArrayList<Earthquake> earthquakes = new ArrayList<>();
        // Get the XML
        URL url;
        try {
          String quakeFeed = getApplication().getString(R.string.earthquake_feed);

          url = new URL(quakeFeed);

          URLConnection connection;
          connection = url.openConnection();
          HttpURLConnection httpConnection = (HttpURLConnection) connection;

          int responseCode = httpConnection.getResponseCode();
          if (responseCode == HttpURLConnection.HTTP_OK) {
            InputStream in = httpConnection.getInputStream();
            DocumentBuilderFactory dbf =
                    DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();

            // Parse the earthquake feed.
            Document dom = db.parse(in);
            Element docEle = dom.getDocumentElement();

            // Get a list of each earthquake entry.
            NodeList nl = docEle.getElementsByTagName("entry");
            if (nl != null && nl.getLength() > 0) {
              for (int i = 0; i < nl.getLength(); i++) {
                Element entry =
                        (Element) nl.item(i);
                Element id =
                        (Element) entry.getElementsByTagName("id").item(0);
                Element title =
                        (Element) entry.getElementsByTagName("title").item(0);
                Element g =
                        (Element) entry.getElementsByTagName("georss:point")
                                .item(0);
                Element when =
                        (Element) entry.getElementsByTagName("updated").item(0);
                Element link =
                        (Element) entry.getElementsByTagName("link").item(0);

                String idString = id.getFirstChild().getNodeValue();
                String details = title.getFirstChild().getNodeValue();
                String hostname = "";//http://earthquake.usgs.gov";
                String linkString = hostname + link.getAttribute("href");
                String point = g.getFirstChild().getNodeValue();
                String dt = when.getFirstChild().getNodeValue();

                SimpleDateFormat sdf =
                        new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS'Z'");
                Date qdate = new GregorianCalendar(0, 0, 0).getTime();

                try {
                  qdate = sdf.parse(dt);
                } catch (ParseException e) {
                  Log.e(AsyncTaskTAG, "Date parsing exception.", e);
                }

                String[] location = point.split(" ");
                Location l = new Location("dummyGPS");
                l.setLatitude(Double.parseDouble(location[0]));
                l.setLongitude(Double.parseDouble(location[1]));

                String magnitudeString = details.split(" ")[1];
                int end = magnitudeString.length() - 1;
                double magnitude =
                        Double.parseDouble(magnitudeString.substring(0, end));

                if (details.contains("-"))
                  details = details.split("-")[1].trim();
                else
                  details = "";

                final Earthquake earthquake = new Earthquake(idString,
                        qdate,
                        details, l,
                        magnitude,
                        linkString);

                // Add the new earthquake to our result array.
                earthquakes.add(earthquake);
              }
            }
          }
          httpConnection.disconnect();


          //在定期作业中检查通知触发条件
          if (params.getJobId()==PERIODIC_UPDATE_TAG_JOB_ID)
          {
            Earthquake largestNewEarthquake = findLargestNewEarthquake(earthquakes);

            SharedPreferences prefs =PreferenceManager.getDefaultSharedPreferences(getApplication());
            int minimumMagnitude = Integer.parseInt(prefs.getString(PreferencesActivity.PREF_MIN_MAG, "3"));

            if (largestNewEarthquake != null && largestNewEarthquake.getMagnitude() >= minimumMagnitude) {
              // 触发一个通知 Trigger a Notification
              broadcastNotification(largestNewEarthquake);
            }
          }

          //todo finihed 测试查看通知样式 broadcastNotification(earthquakes.get(0));


          // Insert the newly parsed array of Earthquakes
          EarthquakeDatabaseAccessor
                  .getInstance(getApplication())
                  .earthquakeDAO()
                  .insertEarthquakes(earthquakes);

          scheduleNextUpdate(getApplication(), params);

          return true;
        } catch (MalformedURLException e) {
          Log.e(AsyncTaskTAG, "Malformed URL Exception", e);
          return false;
        } catch (IOException e) {
          Log.e(AsyncTaskTAG, "IO Exception", e);
          return false;
        } catch (ParserConfigurationException e) {
          Log.e(AsyncTaskTAG, "Parser Configuration Exception", e);
          return false;
        } catch (SAXException e) {
          Log.e(AsyncTaskTAG, "SAX Exception", e);
          return false;
        }









        // Return true if the job succeeded or false if it should be rescheduled due to a transient failure
        //如果作业成功，则返回true；如果由于暂时性故障而应重新安排作业，则返回false
//        return true;
      }

      @Override
      protected void onPostExecute(Boolean success) {
        // Reschedule the job if it did not succeed
        //如果作业未成功，请重新安排作业
        jobFinished(params, !success);
      }
    };
    mJobTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);


    //必须返回true以表示您正在后台工作    // You must return true to signify that you're doing work in the background
    return true;
  }

  @Override
  public boolean onStopJob(JobParameters params) {
    if (mJobTask != null) {
      mJobTask.cancel(true);
    }

    //如果我们不得不中断工作，就重新安排 // If we had to interrupt the job, reschedule it
    return true;


    //如果作业不需要重新安排，则返回false // Return false if the job does not need to be rescheduled
  }


   /**
    * 添加定期任务
    * @param context
    */
   @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
   private static  void scheduleNextUpdate(Context context,  JobParameters job)//
  {
     if (job.getJobId()!=PERIODIC_UPDATE_TAG_JOB_ID)//非定期作业触发时执行
     {
       SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
//更新频率
       int updateFreq = Integer.parseInt(prefs.getString(PreferencesActivity.PREF_UPDATE_FREQ, "60"));
//是否自动更新
       boolean autoUpdateChecked =  prefs.getBoolean(PreferencesActivity.PREF_AUTO_UPDATE, false);



       // 添加\关闭 定期任务
       JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
       if (autoUpdateChecked) {




         // 获取 my Job Service 实现的引用. Get a reference to my Job Service implementation
         ComponentName jobServiceName = new ComponentName(context, BackgroundJobService.class);

         JobInfo.Builder jobInfobuilder = new JobInfo.Builder(PERIODIC_UPDATE_TAG_JOB_ID,  jobServiceName);
         jobInfobuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); //NETWORK_TYPE_UNMETERED需要网络连接 Require a network connection//需要不计量的网络连接，通常是wifi
//         jobInfobuilder.setRequiresDeviceIdle(true); //要求设备处于空闲状态  Require the device has been idle
//         jobInfobuilder.setRequiresCharging(true);//限制作业仅在设备充电时运行
//    todo finihed 检查运行时错误  setPeriodic和setOverrideDeadline互斥，setBackoffCriteria会触发运行时错误
                 jobInfobuilder.setPeriodic(TimeUnit.MINUTES.toMillis(updateFreq));//重复执行设置 updateFreq*60*1000 TimeUnit.MINUTES.toMillis(updateFreq)
         //      jobInfobuilder.setOverrideDeadline(TimeUnit.DAYS.toMillis(1));    // 强制作业在1天后忽略约束//设置启动后多长时间范围内随机开始执行任务
//    会触发运行时错误  jobInfobuilder.setBackoffCriteria(30000, JobInfo.BACKOFF_POLICY_LINEAR);// 30秒后重试，线性后退

//         //   jobInfobuilder.setPersisted(true) ;   //设置是否在设备重新启动期间保留此作业，@see android.app.job.JobInfo 需要权限  Reschedule after the device has been rebooted

         jobScheduler.schedule(jobInfobuilder.build());

//         jobScheduler.schedule(
//                 new JobInfo.Builder(PERIODIC_UPDATE_TAG_JOB_ID,  jobServiceName)
//                         //需要网络连接 Require a network connection//需要不计量的网络连接，通常是wifi
//                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
//                         // .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意的网络连接
//
//                         //要求设备处于空闲状态  Require the device has been idle
//                         .setRequiresDeviceIdle(true)
////                         .setRequiresCharging(true)//限制作业仅在设备通电时运行
//                         // 强制作业在1天后忽略约束
//        似乎setPeriodic和setOverrideDeadline互斥                 .setOverrideDeadline(TimeUnit.DAYS.toMillis(1)) //设置启动后多长时间范围内随机开始执行任务
//                         // 30秒后重试，线性后退
// //    会触发运行时错误                        .setBackoffCriteria(TimeUnit.MILLISECONDS.toMillis(30000), JobInfo.BACKOFF_POLICY_LINEAR)
//                         //设置是否在设备重新启动期间保留此作业，@see android.app.job.JobInfo 需要权限  Reschedule after the device has been rebooted
//                         //   .setPersisted(true)
//                         .setPeriodic(updateFreq*60*1000)//重复执行设置updateFreq*60*1000
//                         .build());
       }else{
         //取消计划的调度任务
         for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) {
           if (jobInfo.getId() == PERIODIC_UPDATE_TAG_JOB_ID ) {
             jobScheduler.cancel(PERIODIC_UPDATE_TAG_JOB_ID);
             break ;
           }
         }
       }
     }
   }
















   /**
    * 用于构造每个通知（NOTIFICATION）的常量
    */
   private static final String NOTIFICATION_CHANNEL = "earthquake_ch11";
   public static final int NOTIFICATION_ID = 1001;

   /**
    * 定义具有振动和灯光的高重要性通知，作为新地震需要高优先级
    */
   private void createNotificationChannel() {
     //仅在运行android8.0，26或更高版本之上的操作系统的设备上创建通知渠道，
     // 在此版本及之上版本也无需单独提供设置通知偏好的界面，应该将用户重定向到系统通知设置页面P274
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
       CharSequence name = getString(R.string.earthquake_channel_name);
       NotificationChannel channel = new NotificationChannel(
               NOTIFICATION_CHANNEL,
               name,
               NotificationManager.IMPORTANCE_HIGH);
       channel.enableVibration(true);//使用默认的振动模式
       channel.enableLights(true);//使用灯光
       NotificationManager notificationManager =getSystemService(NotificationManager.class);
       notificationManager.createNotificationChannel(channel);
     }
   }

   /**
    * 广播通知
    * @param earthquake 地震数据
    */
   private void broadcastNotification(Earthquake earthquake) {

     createNotificationChannel();//创建通知渠道

     Intent startActivityIntent = new Intent(this, EarthquakeMainActivity.class);
     PendingIntent launchIntent = PendingIntent.getActivity(this, 0,
             startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT);//标志表明如果描述的 PendingIntent 已经存在，则保留它，但用这个新 Intent 中的内容替换它的额外数据。

     final NotificationCompat.Builder earthquakeNotificationBuilder
             = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL);

     earthquakeNotificationBuilder
             .setSmallIcon(R.drawable.notification_icon)
             .setColor(ContextCompat.getColor(this, R.color.colorPrimary))
             .setDefaults(NotificationCompat.DEFAULT_ALL)//使用所有灯光振动声音的默认值
             .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
             .setContentIntent(launchIntent)//p276。添加内容用于启动Activity
             .setAutoCancel(true)//在单击通知时关闭通知。也可以设置setDeleteIntent处理用户清除通知
             .setShowWhen(true);

     earthquakeNotificationBuilder
             .setWhen(earthquake.getDate().getTime())
             .setContentTitle("震级:" + earthquake.getMagnitude())
             .setContentText("地震详情"+earthquake.getDetails())
             .setStyle(new NotificationCompat.BigTextStyle()
                     .bigText("地震详情："+ earthquake.getDetails()));//使用扩展的通知样式。API级别16以上支持

     NotificationManagerCompat notificationManager= NotificationManagerCompat.from(this);
//    NotificationManager notificationManager = getSystemService(NotificationManager.class);
     notificationManager.notify(NOTIFICATION_ID,earthquakeNotificationBuilder.build());
   }

   /**
    * 比较两个地震列表，获取需要通知的地震数据，新数据
    * @param newEarthquakes 从网上获取到的新的地震数据，在添加到数据库之前发送通知
    * @return
    */
   private Earthquake findLargestNewEarthquake(List<Earthquake> newEarthquakes) {
     List<Earthquake> earthquakes = EarthquakeDatabaseAccessor
             .getInstance(getApplicationContext())
             .earthquakeDAO()
             .loadAllEarthquakesBlocking();//loadAllEarthquakes

     Earthquake largestNewEarthquake = null;

     for (Earthquake earthquake : newEarthquakes) {
       if (earthquakes.contains(earthquake)) {
         continue;
       }

       if (largestNewEarthquake == null   || earthquake.getMagnitude() >= largestNewEarthquake.getMagnitude()) {
         largestNewEarthquake = earthquake;
         break;
       }
     }
     return largestNewEarthquake;
   }
}